/*
Minims .Net Playstation 3 UPNP ContentDirectory
Copyright (C) 2008  Guillaume Plourde

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2
of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

using System;
using System.Collections.Generic;
using System.Text;

using System.Xml;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Collections;

namespace minims_lib
{
	public class HTTP_SERVER
	{
		private bool Started;
		private int Port;

		private FileManager filemanager;
		private Hashtable services;
		private Thread HttpdThread;
		private Transcoder encoder;
		
		private ConfigManager conf;

		public HTTP_SERVER(Hashtable servs, FileManager _fm, int p)
		{
			this.filemanager = _fm;
			this.services = servs;
			this.Port = p;

			this.conf = ((ConfigManager)services["ConfigManager"]);
		}
		HttpListener listener;

		private void HttpdListen()
		{
			Console.WriteLine("HTTP Server listening on port: {0}", conf.User.Port);

			while (Started)
			{
				HttpListenerContext context = null;
				HttpListenerRequest request = null;

				try
				{
					context = listener.GetContext();
					request = context.Request;
				}
				catch { }

				if (context != null && request != null)
				{
					string address = request.RemoteEndPoint.Address.ToString();
					Console.WriteLine("{0}; {1}", address, request.Headers.Get("X-AV-Client-Info"));

					if (ValidateAccess(address))
						http_respond(context, request);
				}
			}
		}
		public void Start()
		{
			Started = true;
			listener = new HttpListener();
			listener.Prefixes.Add("http://*:" + conf.User.Port + "/");
			//listener.IgnoreWriteExceptions = false;
			listener.Start();
			HttpdThread = new Thread(new ThreadStart(HttpdListen));
			HttpdThread.Start();
		}
		public void Stop()
		{
			if (encoder != null && encoder.Started)
				encoder.Stop();

			Started = false;
			listener.Stop();
		}
		private long[] GetRange(string sz)
		{
			long[] result = new long[2];

			sz = sz.Replace("bytes=", "");

			char[] sep = { '-' };
			string[] tokens = sz.Split(sep);
			long.TryParse(tokens[0], out result[0]);
			long.TryParse(tokens[1], out result[1]);
			return result;
		}
		private void Send(Stream inStream, HttpListenerResponse response, long start, long end)
		{
			try
			{
				response.ProtocolVersion = new Version("1.1");
				response.KeepAlive = true;
				response.ContentLength64 = inStream.Length;
				response.AddHeader("Accept-Ranges", "bytes");

				using (Stream outStream = response.OutputStream)
				{
					Byte[] buffer = new Byte[16384];
					int count = 0;

					inStream.Position = start;

					Console.WriteLine("Send bytes: {0} to {1} of {2}", start, end, inStream.Length);

					while (inStream.Position < end)
					{
						count = inStream.Read(buffer, 0, buffer.Length);
						outStream.Write(buffer, 0, count);
						Console.Write(".");
					}

					Console.WriteLine();
					outStream.Flush();
					outStream.Close();
				}

				response.Close();
			}
			catch (Exception exp) { /*Console.WriteLine(exp.Message);*/ }
		}
		private void Send(byte[] buffer, HttpListenerResponse response, long start, long end)
		{
			try
			{
				response.ProtocolVersion = new Version("1.1");
				response.ContentLength64 = buffer.Length;
				response.KeepAlive = false;
				response.ContentType = "text/xml; charset=\"utf-8\"";
				response.OutputStream.Write(buffer, 0, buffer.Length);
				response.OutputStream.Flush();
				response.OutputStream.Close();
				response.Close();
			}
			catch (Exception exp) { /*Console.WriteLine(exp.Message);*/ }
		}
		private void Stream(FileInfo fi, HttpListenerResponse response)
		{
			response.ProtocolVersion = new Version("1.1");
			response.KeepAlive = true;

			try
			{
				FileStream fs = new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
				BinaryReader br = new BinaryReader(fs);

				long buf;
				int bytes;
				byte[] buffer = new byte[16384];

				using (Stream outStream = response.OutputStream)
				{
					buf = fs.Length;

					while ((bytes = br.Read(buffer, 0, 16384)) > 0)
					{
						// Buffer

						buf += (fs.Length - buf);

						while (buf < 5242880 && encoder.Started)
						{
							buf += (fs.Length - buf);
							Thread.Sleep(1000);
						}

						outStream.Write(buffer, 0, 16384);
						Console.Write(".");
						outStream.Flush();
						buf -= 16384;
					}

					Console.WriteLine();
					outStream.Close();
				}
				response.Close();
			}
			catch (SystemException e) {	/*Console.WriteLine(e);*/ }
		}
		private void http_respond(HttpListenerContext context, HttpListenerRequest request)
		{
			HttpListenerResponse response = context.Response;
			
			if (request.HttpMethod == "SUBSCRIBE")
			{
				if (request.Url.Segments[1] == "System/")
				{
					//Console.WriteLine("System information request from: {0}", request.RemoteEndPoint.Address.ToString());
					//string sysFile = filemanager.GetFile(request.RawUrl);
					//MemoryStream ms = new MemoryStream();
					//BinaryWriter bw = new BinaryWriter(ms);
					//try { bw.Write(Encoding.UTF8.GetBytes(sysFile)); }
					//catch { }
					//Send(ms, response, 0, ms.Length);
					//bw.Close();
					//ms.Close();
				}
			}

			if (request.HttpMethod == "GET")
			{
				if (request.Url.Segments[1] == "System/")
				{
					Console.WriteLine("System information request from: {0}", request.RemoteEndPoint.Address.ToString());
					Console.WriteLine(request.Url.ToString());
					string sysFile = filemanager.GetFile(request.RawUrl);
					byte[] buf = Encoding.UTF8.GetBytes(sysFile);
					Send(buf, response, 0, buf.Length);
				}
				else if (request.Url.Segments[1] == "Media/")
				{
					Console.WriteLine("Media request.");
					int cid = int.Parse(request.Url.Segments[2].Replace("/", ""));
					ContentDirectory cds = ((ContentDirectory)services["ContentDirectory"]);
					Container container = cds.FindContainer(cds.root, cid);
					FileInfo fi = null;
					bool transcode = false;

					foreach (CDS_ITEM item in container.items)
					{
						if (item.classname == "media" && ((MediaItem)item).id.ToString() == request.Url.Segments[3])
						{
							Console.WriteLine(((MediaItem)item).filename);
							fi = new FileInfo(((MediaItem)item).filename);

							if (((MediaItem)item).transcode)
								transcode = true;
						}
					}

					if (transcode)
					{
						// Check encoder
						if (!conf.User.Enable)
							return;

						// spawn encoder process

						if (encoder != null)
						{
							// Check if we need to start a new process or keep encoding the current file

							if (encoder.FileName != fi.FullName || !encoder.Started)
							{
								encoder.Stop();
								encoder = new Transcoder();
								encoder.Start(fi, conf.User.Encoder);
							}
						}
						else
						{
							// Not started so just spawn a new encoding process
							encoder = new Transcoder();
							encoder.Start(fi, conf.User.Encoder);
						}

						// wait for the file to exist and fail after 10 try
						fi = new FileInfo(@"transcodebuf.mpg");

						int retry = 10;

						while (!fi.Exists)
						{
							if (retry < 1)
								return; // Encoding fail

							fi = new FileInfo(@"transcodebuf.mpg");
							Thread.Sleep(1000);
							retry--;
						}

						// start streaming
						Stream(fi, response);
					}
					else
					{
						if (fi.Exists)
						{
							FileStream inStream = new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

							long[] range = { 0, inStream.Length };

							if (request.Headers.Get("Range") != null)
								range = GetRange(request.Headers.Get("Range"));

							if (transcode)
								range[1] = -1;

							if (range[1] == 0)
								range[1] = inStream.Length;

							Send(inStream, response, range[0], range[1]);
						}
					}
				}
			}
			else if (request.HttpMethod == "POST")
			{
				StringBuilder sb = new StringBuilder();

				XmlDocument querybody;
				String schemas;
				String query;

				ParseSoapAction(request.Headers.Get("SOAPACTION"), out schemas, out query);

				StreamReader sr = new StreamReader(request.InputStream);

				querybody = new XmlDocument();
				querybody.LoadXml(sr.ReadToEnd());

				XmlNamespaceManager nsmgr = new XmlNamespaceManager(querybody.NameTable);
				nsmgr.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/");
				nsmgr.AddNamespace("u", schemas);

				if (query.Contains("GetSearchCapabilities"))
					sb.Append(((ContentDirectory)services["ContentDirectory"]).GetSearchCapabilities(schemas));
				else if (query.Contains("GetSortCapabilities"))
					sb.Append(((ContentDirectory)services["ContentDirectory"]).GetSortCapabilities(schemas));
				else if (query.Contains("GetSystemUpdateID"))
					sb.Append(((ContentDirectory)services["ContentDirectory"]).GetSystemUpdateID(schemas));
				else if (query.Contains("Browse"))
				{
					int ObjectID = 0;

					try
					{
						ObjectID = int.Parse(querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/ObjectID", nsmgr).InnerText);
					}
					catch
					{
						ObjectID = int.Parse(querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/ContainerID", nsmgr).InnerText);
					}

					String BrowserFlag = querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/BrowseFlag", nsmgr).InnerText;
					String Filter = querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/Filter", nsmgr).InnerText;
					int StartingIndex = int.Parse(querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/StartingIndex", nsmgr).InnerText);
					int RequestedCount = int.Parse(querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/RequestedCount", nsmgr).InnerText);
					String SortCriteria = querybody.SelectSingleNode("/s:Envelope/s:Body/u:Browse/SortCriteria", nsmgr).InnerText;
					Console.WriteLine("{0} {1} {2} {3} {4} {5} ", ObjectID, BrowserFlag, Filter, StartingIndex, RequestedCount, SortCriteria);
					sb.Append(((ContentDirectory)services["ContentDirectory"]).Browse(ObjectID, BrowserFlag, Filter, StartingIndex, RequestedCount, SortCriteria, schemas));
				}

				response = context.Response;
				response.ContentType = " text/xml";

				MemoryStream ms = new MemoryStream();
				BinaryWriter bw = new BinaryWriter(ms);
				byte[] buf = Encoding.UTF8.GetBytes(sb.ToString());
				Send(buf, response, 0, buf.Length);
				//bw.Close();
				//ms.Close();
				Console.WriteLine("Post done.");
			}
		}
		static void ParseSoapAction(String raw, out String schemas, out String query)
		{
			raw = raw.Replace("\"", "");
			string[] tokens = raw.Split(new char[] { '#' });
			schemas = tokens[0];
			query = tokens[1];
		}

		//

		private bool ValidateAccess(string address)
		{
			bool access = true;
			string[] sep0 = { ";" };
			string[] sep1 = { "." };

			// Parse allowed address range
			string[] allowed = conf.User.Allow.Split(sep0, StringSplitOptions.RemoveEmptyEntries);
			string[] denied = conf.User.Deny.Split(sep0, StringSplitOptions.RemoveEmptyEntries);
			string[] parts = address.Split(sep1, StringSplitOptions.RemoveEmptyEntries);

			foreach (string s in denied)
			{
				string[] d_parts = s.Split(sep1, StringSplitOptions.RemoveEmptyEntries);

				if (s == "*")
				{
					access = false;
					continue;
				}
				else if (d_parts.Length == 4 && ((d_parts[0] == parts[0]) && (d_parts[1] == parts[1]) && (d_parts[2] == parts[2]) && (d_parts[3] == parts[3])))
				{
					access = false;
				}
			}

			foreach (string s in allowed)
			{
				string[] a_parts = s.Split(sep1, StringSplitOptions.RemoveEmptyEntries);

				if (s == "*")
				{
					access = true;
					continue;
				}
				else if (a_parts.Length == 4 && ((a_parts[0] == parts[0] || a_parts[0] == "*") && (a_parts[1] == parts[1] || a_parts[1] == "*") && (a_parts[2] == parts[2] || a_parts[2] == "*") && (a_parts[3] == parts[3] || a_parts[3] == "*")))
				{
					access = true;
				}
			}

			return access;
		}
	}
}